﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Platform.Infrastructure;
using Platform.ObjectModel.Design;
using Platform.ObjectModel.Validation;
using Platform.Resources;
using Platform.Storage.Entity;

namespace Platform.ObjectModel
{

	/// <summary>
	/// Mantém a lista de schemas que foram carregados
	/// </summary>
	public class LibrarySchemaWorkspace
	{
		/// <summary>
		/// Slots para armazenar os schemas carregados e a relação entre os tipos .net
		/// </summary>
		private List<WorkspaceBucket> buckets = new List<WorkspaceBucket>();

		/// <summary>
		/// Lista de bindings que foram especificados mas que ainda o Schema não foi carregado
		/// </summary>
		private List<Tuple<DataObjectName, Type>> pendingToBind = new List<Tuple<DataObjectName, Type>>();

		/// <summary>
		/// 
		/// </summary>
		private List<BindSchemaMissing> schemaMissing = new List<BindSchemaMissing>();


		/// <summary>
		/// 
		/// </summary>
		internal LibrarySchemaWorkspace( Library lib )
		{
			Debug.Assert( lib != null );
			this.Library = lib;
		}

		/// <summary>
		/// Obtém a lista com todas as associações entre um tipo e um schema
		/// </summary>
		internal IEnumerable<WorkspaceBucket> Buckets
		{ get { return this.buckets; } }

		/// <summary>
		/// Obtém a bibliotece de que pertence
		/// </summary>
		public Library Library
		{ get; private set; }


		/// <summary>
		/// Adiciona o schema no cache, e procura pelo tipo .net que deve ser carregado
		/// </summary>
		/// <param name="schema"></param>
		private void Add( Schema schema )
		{
			Debug.Assert( schema != null );

			// Verifica se já foi feito o binding anteriormente
			var typeMissing = this.schemaMissing.Where( s => s.Name.Equals( schema.QualifiedName ) ).SingleOrDefault();

			if ( typeMissing == null )
				this.Add( typeof( DataObjectFlake ), schema );
			else
				this.Add( typeMissing.EntityType, schema );
		}

		/// <summary>
		/// Somente adiciona um schema na lista de schemas carregados em memória
		/// </summary>
		/// <param name="entity"></param>
		/// <param name="schema"></param>
		internal void Add( Type entity, Schema schema )
		{
			Debug.Assert( entity != null );
			Debug.Assert( schema != null );

			var bucket = new WorkspaceBucket() { Schema = schema, Type = entity };
			this.buckets.Add( bucket );
		}

		/// <summary>
		/// Adiciona no banco de dados o novo schema
		/// </summary>
		/// <param name="schema"></param>
		public void RegisterSchema( Schema schema )
		{
			this.RegisterSchema( typeof( DocumentFlake ), schema );
		}

		/// <summary>
		/// Adiciona no banco de dados o <paramref name="schema"/> e associa à entidade <paramref name="entityType"/>
		/// </summary>
		/// <param name="entityType"></param>
		/// <param name="schema"></param>
		public void RegisterSchema( Type entityType, Schema schema )
		{
			Arguments.IsNotNull( "schema", schema );
			Arguments.IsNotNull( "entityType", entityType );

			if ( !entityType.IsOrInheritFrom( typeof( DataObjectFlake ) ) )
			{
				throw Exceptions.TypeIsNotDocument( entityType );
			}

			ValidationResult result = this.Library.PutInternal( schema, Schema.CreateSchemaForDocumentSchema() );

			if ( !result.IsValid )
				throw Exceptions.ValidationResultError( result );
			this.Add( entityType, schema );
		}

		/// <summary>
		/// Obtém o schema pelo nome
		/// </summary>
		/// <param name="qualifiedName"></param>
		/// <returns></returns>
		public Schema GetSchema( string qualifiedName )
		{
			return this.GetSchema( DataObjectName.Parse( qualifiedName ) );
		}

		/// <summary>
		/// Obtém o schema para o tipo <paramref name="entity"/>
		/// </summary>
		/// <param name="entity"></param>
		/// <returns></returns>
		public Schema GetSchema( Type entity )
		{
			var bucket = this.buckets.Where( w => w.Type.Equals( entity ) ).FirstOrDefault();

			if ( bucket != null )
				return bucket.Schema;
			else
				return null;
		}

		/// <summary>
		/// Registra um novo schema para ser utilizado pela biblioteca
		/// </summary>
		/// <param name="name"></param>
		/// <returns></returns>
		public Schema GetSchema( DataObjectName name )
		{
			// TODO: Schema
			// 1. Carrega o schema
			// 2. Carrega os javascript
			// 3. Carrega os objetos dinâmicos

			Arguments.IsNotNull( "name", name );
			Schema schema =
				this.Buckets
					.Select( b => b.Schema )
					.FirstOrDefault( s => s.QualifiedName.Equals( name ) );

			if ( schema == null )
			{
				var query = this.Library.Storage.Get<Schema>().EagerLoad( s => s.Fields );
				var l = query.ToList();
				schema = query.FirstOrDefault();
				//schema = this.Library
				//	.Storage
				//	.Get<Schema>()
				//	.FirstOrDefault( s => s.QualifiedName == name );
				//TODO : .Apply( new LoadAllRelations() )
					

				if ( schema == null )
					throw Exceptions.SchemaNotFound( name );
				this.Add( schema );
			}

			return schema;
		}
		

		/// <summary>
		/// Associa o tipo <paramref name="entityType"/> ao schema <paramref name="schemaName"/>
		/// </summary>
		/// <param name="entityType"></param>
		/// <param name="schemaName"></param>
		public void BindType( Type entityType, DataObjectName schemaName )
		{
			Arguments.IsNotNull( "entityType", entityType );
			Arguments.IsNotNull( "schemaName", schemaName );
			
			// Se o schema já foi carregado anteriormente, então
			// somente atualiza o tipo .net que deve ser utilizado para 
			// o schema
			int count = 0;
			this.buckets
				.Where( bucket => bucket.Schema.QualifiedName.Equals( schemaName ) )
				.Each( b =>
					{
						b.Type = entityType;
						count++;
					} );

			//
			switch ( count )
			{
				case 0:	// O schema ainda não foi carregado do banco de dados
					this.schemaMissing.Add( new BindSchemaMissing( entityType, schemaName ) );
					break;

				case 1:	// Já achou o schema que foi carregado anteriormente e já está atualizado
					break;

				default:
					Debug.Fail( String.Format( "Foi carregado mais de um schema para o nome \"{0}\".", schemaName.FullName ) );
					break;
			}
		}

		/// <summary>
		/// Obtém o tipo .Net que está associado ao schema <paramref name="schemaName"/>
		/// </summary>
		/// <param name="schemaName"></param>
		/// <returns></returns>
		public Type GetDocType( string schemaName )
		{
			Arguments.IsNotNullOrEmpty( "schemaName", schemaName );
			return this.GetDocType( DataObjectName.Parse( schemaName ) );
		}

		/// <summary>
		/// Obtém o tipo .Net que está associado ao schema <paramref name="schemaName"/>.
		/// </summary>
		/// <param name="schemaName"></param>
		/// <returns></returns>
		public Type GetDocType( DataObjectName schemaName )
		{
			Arguments.IsNotNull( "schemaName", schemaName );

			var bucket = this.buckets.FirstOrDefault( b => b.Schema.QualifiedName.Equals( schemaName ) );

			if ( bucket == null )
				return typeof( DocumentFlake );
			else
				return bucket.Type;
		}

		#region [ class WorkspaceBucket ]

		/// <summary>
		/// Slot contendo a relação entre o tipo .net e o schema do documento
		/// </summary>
		internal sealed class WorkspaceBucket
		{
			public WorkspaceBucket()
			{ }

			/// <summary>
			/// Obtém ou define o schema 
			/// </summary>
			public Schema Schema
			{ get; set; }

			/// <summary>
			/// Obtém ou define o tipo .net que é utilizado para representar o documento
			/// </summary>
			public Type Type
			{ get; set; }
		}

		#endregion

		#region [ class BindSchemaMissing ]

		/// <summary>
		/// Representa uma associação entre um tipo .net e um nome do schema que representa
		/// esse tipo
		/// </summary>
		internal sealed class BindSchemaMissing
		{

			public BindSchemaMissing( Type entityType, DataObjectName schemaName )
			{
				Debug.Assert( entityType != null );
				Debug.Assert( schemaName != null );

				this.EntityType = entityType;
				this.Name = schemaName;
			}

			public Type EntityType
			{ get; private set; }

			public DataObjectName Name
			{ get; private set; }
		}

		#endregion
	}


	public sealed class LoadAllDocumentRelations : IQueryStrategy<DocumentFlake>
	{

		#region IQueryStrategy<DocumentFlake> Members

		public IQueryable<DocumentFlake> Configure( IQueryable<DocumentFlake> query )
		{
			EntityDynamicQuery<DocumentFlake> dynQuery = query as EntityDynamicQuery<DocumentFlake>;
			return null;
		}

		#endregion
	}

	//public class LoadAllDocumentRelations<TDocument> : IQueryStrategy<TDocument>
	//{

	//    //public LoadAllDocumentRelations( Schema schema )
	//    //{
	//    //    Arguments.IsNotNull( "schema", schema );
	//    //    this.Schema = schema;
	//    //}

	//    //public Schema Schema
	//    //{ get; private set; }

	//    #region IQueryStrategy<Schema> Members

	//    public IQueryable<TDocument> Configure( IQueryable<TDocument> query )
	//    {
			
	//    }

	//    #endregion
	//}
}
